Skip to content

fix: keep the provider user agent when called through the AI SDK - #603

Open
LukasParke wants to merge 2 commits into
mainfrom
claude/user-agent-suffix
Open

LukasParke wants to merge 2 commits into
mainfrom
claude/user-agent-suffix

Conversation

@LukasParke

@LukasParke LukasParke commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Description

Bug. Requests made through the AI SDK lose the provider's ai-sdk/openrouter/<version> user agent identifier. The AI SDK passes its own user-agent (ai/<version>) in the call-level headers, and every model merged headers with combineHeaders(this.config.headers(), options.headers), which puts call headers last. So the SDK's value replaced ours. A direct doGenerate call has no call-level UA, so it kept the identifier, which is why the existing tests didn't catch this.

Repro.

const openrouter = createOpenRouter({ apiKey, fetch: logUserAgent });
await generateText({ model: openrouter('openai/gpt-4o-mini'), prompt: 'hi' });
// user-agent contains ai/7.0.103 but not ai-sdk/openrouter/<version>

What other providers do. @ai-sdk/openai (latest) builds its config headers the same way, with withUserAgentSuffix({...}, 'ai-sdk/openai/<v>') and then combineHeaders(this.config.headers?.(), options.headers). combineHeaders in @ai-sdk/provider-utils (latest) is still a plain spread, so first-party providers lose their identifier through ai too. Running withUserAgentSuffix after combining won't work here either: our version (#362, fixes #300) uses any existing UA verbatim, so it would just keep ai/<v>.

Fix. New combineHeadersWithUserAgent(configHeaders, callHeaders) in src/utils/with-user-agent-suffix.ts. It combines the headers like before. When the config UA is the default provider identifier and the call-level UA comes from the AI SDK (it contains an ai/<version> token) without the identifier, it sends <call UA> ai-sdk/openrouter/<v>. For example, ai/7.0.103 ai-sdk/openrouter/3.1.0 ai-sdk/provider-utils/... runtime/....

  • A user-specified provider-level User-Agent (Openrouter AI SDK provider overrides/silently rewrites user-specified headers #300) is still never modified.
  • A custom call-level UA without the ai/<v> token, e.g. doGenerate({ headers: { 'user-agent': 'my-app/1.0' } }), is still sent verbatim (raised in review). Through ai, a user's call-level UA becomes my-app/1.0 ai/<v>, and that one does get the identifier appended.
  • The identifier is never added twice.

It replaces combineHeaders everywhere headers are combined: chat (doGenerate, doStream), completion (doGenerate, doStream), embedding, image, video, reranking and evaluation.

Not changed. Through ai, a user-specified provider-level User-Agent is still replaced by the SDK's call UA. That is existing behavior and a separate question from this bug.

Tests.

  • src/tests/user-agent.test.ts drives the real ai functions with a mocked fetch and asserts the final UA contains both ai/<v> and ai-sdk/openrouter/<v>: generateText (chat and completion), embed, generateImage, experimental_generateVideo, experimental_evaluate. streamText and rerank in the pinned ai 7.0.103 send no ai/<v> UA, so for those the test asserts that the provider identifier is present. It also checks that a user-specified provider UA, and a custom call-level UA on a direct doGenerate, don't get the identifier appended.
  • src/utils/with-user-agent-suffix.test.ts adds unit tests for the helper: append, case-insensitive match, no call UA, no duplication, custom call UA without the ai/ token, custom provider UA.

I checked that the 6 ai-driven assertions (chat, completion, embed, image, video, evaluate) fail on origin/main and pass with this fix.

Checklist

  • I have run pnpm stylecheck and pnpm typecheck
  • I have run pnpm test and all tests pass
  • I have added tests for my changes (if applicable)
  • I have updated documentation (if applicable)

Changeset

  • I have run pnpm changeset to create a changeset file

🤖 Generated with Claude Code

devin-ai-integration[bot]

This comment was marked as resolved.

LukasParke added a commit that referenced this pull request Oct 23, 2026
…lict with #603

The image model already shallow merges provider routing (#551); moving it
onto mergeProviderRouting was a pure refactor, and its new import landed on
the same line as #603's user-agent import. Keep image/index.ts as on main so
the two PRs merge in either order. No behaviour change.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
LukasParke and others added 2 commits November 10, 2026 13:11
The AI SDK passes its own user-agent call header, and call headers were
merged last with combineHeaders, so the ai-sdk/openrouter/<version>
identifier was dropped for generateText, embed, generateImage and the rest.
Headers are now combined with combineHeadersWithUserAgent, which appends the
identifier to the call-level user agent.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A custom call-level user-agent on a direct model call is used verbatim
again; the identifier is only appended when the call-level value carries
the AI SDK's ai/<version> token.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@LukasParke
LukasParke force-pushed the claude/user-agent-suffix branch from a34c2b6 to 9c81ee0 Compare November 10, 2026 19:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Openrouter AI SDK provider overrides/silently rewrites user-specified headers

1 participant